package com.hoho.android.usbserial.driver;


import android.hardware.usb.UsbConstants;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbEndpoint;
import android.hardware.usb.UsbInterface;
import android.hardware.usb.UsbRequest;
import android.util.Log;

import com.hoho.android.usbserial.util.MonotonicClock;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.LinkedHashMap;
import java.util.Map;


public class NewProlificSerialDriver extends CommonUsbSerialDriver {

    private final static int[] standardBaudRates = {
            75, 150, 300, 600, 1200, 1800, 2400, 3600, 4800, 7200, 9600, 14400, 19200,
            28800, 38400, 57600, 115200, 128000, 134400, 161280, 201600, 230400, 268800,
            403200, 460800, 614400, 806400, 921600, 1228800, 2457600, 3000000, 6000000
    };
    private static final int USB_READ_TIMEOUT_MILLIS = 1000;
    private static final int USB_WRITE_TIMEOUT_MILLIS = 5000;

    private static final int USB_RECIP_INTERFACE = 0x01;

    private static final int VENDOR_READ_REQUEST = 0x01;
    private static final int VENDOR_WRITE_REQUEST = 0x01;
    private static final int VENDOR_READ_HXN_REQUEST = 0x81;
    private static final int VENDOR_WRITE_HXN_REQUEST = 0x80;

    private static final int VENDOR_OUT_REQTYPE = UsbConstants.USB_DIR_OUT | UsbConstants.USB_TYPE_VENDOR;
    private static final int VENDOR_IN_REQTYPE = UsbConstants.USB_DIR_IN | UsbConstants.USB_TYPE_VENDOR;
    private static final int CTRL_OUT_REQTYPE = UsbConstants.USB_DIR_OUT | UsbConstants.USB_TYPE_CLASS | USB_RECIP_INTERFACE;

    private static final int WRITE_ENDPOINT = 0x02;
    private static final int READ_ENDPOINT = 0x83;
    private static final int INTERRUPT_ENDPOINT = 0x81;

    private static final int RESET_HXN_REQUEST = 0x07;
    private static final int FLUSH_RX_REQUEST = 0x08;
    private static final int FLUSH_TX_REQUEST = 0x09;
    private static final int SET_LINE_REQUEST = 0x20; // same as CDC SET_LINE_CODING
    private static final int SET_CONTROL_REQUEST = 0x22; // same as CDC SET_CONTROL_LINE_STATE
    private static final int SEND_BREAK_REQUEST = 0x23; // same as CDC SEND_BREAK
    private static final int GET_CONTROL_HXN_REQUEST = 0x80;
    private static final int GET_CONTROL_REQUEST = 0x87;
    private static final int STATUS_NOTIFICATION = 0xa1; // similar to CDC SERIAL_STATE but different length

    /* RESET_HXN_REQUEST */
    private static final int RESET_HXN_RX_PIPE = 1;
    private static final int RESET_HXN_TX_PIPE = 2;

    /* SET_CONTROL_REQUEST */
    private static final int CONTROL_DTR = 0x01;
    private static final int CONTROL_RTS = 0x02;

    /* GET_CONTROL_REQUEST */
    private static final int GET_CONTROL_FLAG_CD = 0x02;
    private static final int GET_CONTROL_FLAG_DSR = 0x04;
    private static final int GET_CONTROL_FLAG_RI = 0x01;
    private static final int GET_CONTROL_FLAG_CTS = 0x08;

    /* GET_CONTROL_HXN_REQUEST */
    private static final int GET_CONTROL_HXN_FLAG_CD = 0x40;
    private static final int GET_CONTROL_HXN_FLAG_DSR = 0x20;
    private static final int GET_CONTROL_HXN_FLAG_RI = 0x80;
    private static final int GET_CONTROL_HXN_FLAG_CTS = 0x08;

    /* interrupt endpoint read */
    private static final int STATUS_FLAG_CD = 0x01;
    private static final int STATUS_FLAG_DSR = 0x02;
    private static final int STATUS_FLAG_RI = 0x08;
    private static final int STATUS_FLAG_CTS = 0x80;

    private static final int STATUS_BUFFER_SIZE = 10;
    private static final int STATUS_BYTE_IDX = 8;

    protected enum DeviceType {DEVICE_TYPE_01, DEVICE_TYPE_T, DEVICE_TYPE_HX, DEVICE_TYPE_HXN}

    protected DeviceType mDeviceType = DeviceType.DEVICE_TYPE_HX;
    private UsbEndpoint mInterruptEndpoint;
    private int mControlLinesValue = 0;
    private int mBaudRate = -1, mDataBits = -1, mStopBits = -1, mParity = -1;

    private int mStatus = 0;
    private volatile Thread mReadStatusThread = null;
    private final Object mReadStatusThreadLock = new Object();
    private boolean mStopReadStatusThread = false;
    private IOException mReadStatusException = null;


    private final String TAG = NewProlificSerialDriver.class.getSimpleName();

    protected UsbRequest mUsbRequest;
    protected UsbEndpoint mReadEndpoint;
    protected UsbEndpoint mWriteEndpoint;


    public NewProlificSerialDriver(UsbDevice device, UsbDeviceConnection connection) {
        super(device, connection);
    }

    @Override
    public void open() throws IOException {
        try {
            openInt(mConnection);
            if (mReadEndpoint == null || mWriteEndpoint == null) {
                throw new IOException("Could not get read & write endpoints");
            }
            mUsbRequest = new UsbRequest();
            mUsbRequest.initialize(mConnection, mReadEndpoint);
        } catch (Exception e) {
            try {
                close();
            } catch (Exception ignored) {
            }
            throw e;
        }
    }

    private void openInt(UsbDeviceConnection connection) throws IOException {
        UsbInterface usbInterface = mDevice.getInterface(0);

        if (!connection.claimInterface(usbInterface, true)) {
            throw new IOException("Error claiming Prolific interface 0");
        }

        for (int i = 0; i < usbInterface.getEndpointCount(); ++i) {
            UsbEndpoint currentEndpoint = usbInterface.getEndpoint(i);

            switch (currentEndpoint.getAddress()) {
                case READ_ENDPOINT:
                    mReadEndpoint = currentEndpoint;
                    break;

                case WRITE_ENDPOINT:
                    mWriteEndpoint = currentEndpoint;
                    break;

                case INTERRUPT_ENDPOINT:
                    mInterruptEndpoint = currentEndpoint;
                    break;
            }
        }

        byte[] rawDescriptors = connection.getRawDescriptors();
        if (rawDescriptors == null || rawDescriptors.length < 14) {
            throw new IOException("Could not get device descriptors");
        }
        int usbVersion = (rawDescriptors[3] << 8) + rawDescriptors[2];
        int deviceVersion = (rawDescriptors[13] << 8) + rawDescriptors[12];
        byte maxPacketSize0 = rawDescriptors[7];
        if (mDevice.getDeviceClass() == 0x02 || maxPacketSize0 != 64) {
            mDeviceType = DeviceType.DEVICE_TYPE_01;
        } else if (usbVersion == 0x200) {
            if (deviceVersion == 0x300 && testHxStatus()) {
                mDeviceType = DeviceType.DEVICE_TYPE_T; // TA
            } else if (deviceVersion == 0x500 && testHxStatus()) {
                mDeviceType = DeviceType.DEVICE_TYPE_T; // TB
            } else {
                mDeviceType = DeviceType.DEVICE_TYPE_HXN;
            }
        } else {
            mDeviceType = DeviceType.DEVICE_TYPE_HX;
        }
        Log.d(TAG, String.format("usbVersion=%x, deviceVersion=%x, deviceClass=%d, packetSize=%d => deviceType=%s",
                usbVersion, deviceVersion, mDevice.getDeviceClass(), maxPacketSize0, mDeviceType.name()));
        resetDevice();
        doBlackMagic();
        setControlLines(mControlLinesValue);
    }

    private boolean testHxStatus() {
        try {
            inControlTransfer(VENDOR_IN_REQTYPE, VENDOR_READ_REQUEST, 0x8080, 0, 1);
            return true;
        } catch (IOException ignored) {
            return false;
        }
    }

    private byte[] inControlTransfer(int requestType, int request, int value, int index, int length) throws IOException {
        byte[] buffer = new byte[length];
        int result = mConnection.controlTransfer(requestType, request, value, index, buffer, length, USB_READ_TIMEOUT_MILLIS);
        if (result != length) {
            throw new IOException(String.format("ControlTransfer %s 0x%x failed: %d", mDeviceType.name(), value, result));
        }
        return buffer;
    }

    private void resetDevice() throws IOException {
        purgeHwBuffers(true, true);
    }


    private void doBlackMagic() throws IOException {
        if (mDeviceType == DeviceType.DEVICE_TYPE_HXN)
            return;
        vendorIn(0x8484, 0, 1);
        vendorOut(0x0404, 0, null);
        vendorIn(0x8484, 0, 1);
        vendorIn(0x8383, 0, 1);
        vendorIn(0x8484, 0, 1);
        vendorOut(0x0404, 1, null);
        vendorIn(0x8484, 0, 1);
        vendorIn(0x8383, 0, 1);
        vendorOut(0, 1, null);
        vendorOut(1, 0, null);
        vendorOut(2, (mDeviceType == DeviceType.DEVICE_TYPE_01) ? 0x24 : 0x44, null);
    }


    private byte[] vendorIn(int value, int index, int length) throws IOException {
        int request = (mDeviceType == DeviceType.DEVICE_TYPE_HXN) ? VENDOR_READ_HXN_REQUEST : VENDOR_READ_REQUEST;
        return inControlTransfer(VENDOR_IN_REQTYPE, request, value, index, length);
    }

    private void vendorOut(int value, int index, byte[] data) throws IOException {
        int request = (mDeviceType == DeviceType.DEVICE_TYPE_HXN) ? VENDOR_WRITE_HXN_REQUEST : VENDOR_WRITE_REQUEST;
        outControlTransfer(VENDOR_OUT_REQTYPE, request, value, index, data);
    }

    private void outControlTransfer(int requestType, int request, int value, int index, byte[] data) throws IOException {
        int length = (data == null) ? 0 : data.length;
        int result = mConnection.controlTransfer(requestType, request, value, index, data, length, USB_WRITE_TIMEOUT_MILLIS);
        if (result != length) {
            throw new IOException(String.format("ControlTransfer %s 0x%x failed: %d", mDeviceType.name(), value, result));
        }
    }


    private void setControlLines(int newControlLinesValue) throws IOException {
        ctrlOut(SET_CONTROL_REQUEST, newControlLinesValue, 0, null);
        mControlLinesValue = newControlLinesValue;
    }

    private void ctrlOut(int request, int value, int index, byte[] data) throws IOException {
        outControlTransfer(CTRL_OUT_REQTYPE, request, value, index, data);
    }

    @Override
    public void close() throws IOException {
        if (mConnection == null) {
            throw new IOException("Already closed");
        }
        try {
            mUsbRequest.cancel();
        } catch (Exception ignored) {
        }
        mUsbRequest = null;
        try {
            closeInt();
        } catch (Exception ignored) {
        }
        try {
            mConnection.close();
        } catch (Exception ignored) {
        }
    }

    public void closeInt() {
        try {
            synchronized (mReadStatusThreadLock) {
                if (mReadStatusThread != null) {
                    try {
                        mStopReadStatusThread = true;
                        mReadStatusThread.join();
                    } catch (Exception e) {
                        Log.w(TAG, "An error occured while waiting for status read thread", e);
                    }
                    mStopReadStatusThread = false;
                    mReadStatusThread = null;
                    mReadStatusException = null;
                }
            }
            resetDevice();
        } catch (Exception ignored) {
        }
        try {
            mConnection.releaseInterface(mDevice.getInterface(0));
        } catch (Exception ignored) {
        }
    }

    @Override
    public int read(byte[] dest, int timeoutMillis) throws IOException {
        return read(dest, timeoutMillis, true);
    }

    protected int read(final byte[] dest, final int timeout, boolean testConnection) throws IOException {
        if (mConnection == null) {
            throw new IOException("Connection closed");
        }
        if (dest.length <= 0) {
            throw new IllegalArgumentException("Read buffer to small");
        }
        final int nread;
        if (timeout != 0) {
            // bulkTransfer will cause data loss with short timeout + high baud rates + continuous transfer
            //   https://stackoverflow.com/questions/9108548/android-usb-host-bulktransfer-is-losing-data
            // but mConnection.requestWait(timeout) available since Android 8.0 es even worse,
            // as it crashes with short timeout, e.g.
            //   A/libc: Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x276a in tid 29846 (pool-2-thread-1), pid 29618 (.usbserial.test)
            //     /system/lib64/libusbhost.so (usb_request_wait+192)
            //     /system/lib64/libandroid_runtime.so (android_hardware_UsbDeviceConnection_request_wait(_JNIEnv*, _jobject*, long)+84)
            // data loss / crashes were observed with timeout up to 200 msec
            long endTime = testConnection ? MonotonicClock.millis() + timeout : 0;
            int readMax = Math.min(dest.length, DEFAULT_READ_BUFFER_SIZE);
            nread = mConnection.bulkTransfer(mReadEndpoint, dest, readMax, timeout);
            // Android error propagation is improvable:
            //  nread == -1 can be: timeout, connection lost, buffer to small, ???
            if (nread == -1 && testConnection && MonotonicClock.millis() < endTime)
                testConnection();

        } else {
            final ByteBuffer buf = ByteBuffer.wrap(dest);
            if (!mUsbRequest.queue(buf, dest.length)) {
                throw new IOException("Queueing USB request failed");
            }
            final UsbRequest response = mConnection.requestWait();
            if (response == null) {
                throw new IOException("Waiting for USB request failed");
            }
            nread = buf.position();
            // Android error propagation is improvable:
            //   response != null & nread == 0 can be: connection lost, buffer to small, ???
            if (nread == 0) {
                testConnection();
            }
        }
        return Math.max(nread, 0);
    }

    /**
     * use simple USB request supported by all devices to test if connection is still valid
     */
    private void testConnection() throws IOException {
        byte[] buf = new byte[2];
        int len = mConnection.controlTransfer(0x80 /*DEVICE*/, 0 /*GET_STATUS*/, 0, 0, buf, buf.length, 200);
        if (len < 0)
            throw new IOException("USB get_status request failed");
    }

    @Override
    public int write(byte[] src, int timeoutMillis) throws IOException {
        int offset = 0;
        final long endTime = (timeoutMillis == 0) ? 0 : (MonotonicClock.millis() + timeoutMillis);

        if(mConnection == null) {
            throw new IOException("Connection closed");
        }
        while (offset < src.length) {
            int requestTimeout;
            final int requestLength;
            final int actualLength;

            synchronized (mWriteBufferLock) {
                final byte[] writeBuffer;

                if (mWriteBuffer == null) {
                    mWriteBuffer = new byte[mWriteEndpoint.getMaxPacketSize()];
                }
                requestLength = Math.min(src.length - offset, mWriteBuffer.length);
                if (offset == 0) {
                    writeBuffer = src;
                } else {
                    // bulkTransfer does not support offsets, make a copy.
                    System.arraycopy(src, offset, mWriteBuffer, 0, requestLength);
                    writeBuffer = mWriteBuffer;
                }
                if (timeoutMillis == 0 || offset == 0) {
                    requestTimeout = timeoutMillis;
                } else {
                    requestTimeout = (int)(endTime - MonotonicClock.millis());
                    if(requestTimeout == 0)
                        requestTimeout = -1;
                }
                if (requestTimeout < 0) {
                    actualLength = -2;
                } else {
                    actualLength = mConnection.bulkTransfer(mWriteEndpoint, writeBuffer, requestLength, requestTimeout);
                }
            }
//            if (BuildConfig.DEBUG) {
//                Log.d(TAG, "Wrote " + actualLength + "/" + requestLength + " offset " + offset + "/" + src.length + " timeout " + requestTimeout);
//            }
            if (actualLength <= 0) {
                if (timeoutMillis != 0 && MonotonicClock.millis() >= endTime) {
                    SerialTimeoutException ex = new SerialTimeoutException("Error writing " + requestLength + " bytes at offset " + offset + " of total " + src.length + ", rc=" + actualLength);
                    ex.bytesTransferred = offset;
                    throw ex;
                } else {
                    throw new IOException("Error writing " + requestLength + " bytes at offset " + offset + " of total " + src.length);
                }
            }
            offset += actualLength;
        }
        return offset;
    }

    @Override
    public void setParameters(int baudRate, int dataBits, int stopBits, int parity) throws IOException {
        baudRate = filterBaudRate(baudRate);
        if ((mBaudRate == baudRate) && (mDataBits == dataBits)
                && (mStopBits == stopBits) && (mParity == parity)) {
            // Make sure no action is performed if there is nothing to change
            return;
        }

        byte[] lineRequestData = new byte[7];
        lineRequestData[0] = (byte) (baudRate & 0xff);
        lineRequestData[1] = (byte) ((baudRate >> 8) & 0xff);
        lineRequestData[2] = (byte) ((baudRate >> 16) & 0xff);
        lineRequestData[3] = (byte) ((baudRate >> 24) & 0xff);

        switch (stopBits) {
            case STOPBITS_1:
                lineRequestData[4] = 0;
                break;
            case STOPBITS_1_5:
                lineRequestData[4] = 1;
                break;
            case STOPBITS_2:
                lineRequestData[4] = 2;
                break;
            default:
                throw new IllegalArgumentException("Invalid stop bits: " + stopBits);
        }

        switch (parity) {
            case PARITY_NONE:
                lineRequestData[5] = 0;
                break;
            case PARITY_ODD:
                lineRequestData[5] = 1;
                break;
            case PARITY_EVEN:
                lineRequestData[5] = 2;
                break;
            case PARITY_MARK:
                lineRequestData[5] = 3;
                break;
            case PARITY_SPACE:
                lineRequestData[5] = 4;
                break;
            default:
                throw new IllegalArgumentException("Invalid parity: " + parity);
        }

        if(dataBits < DATABITS_5 || dataBits > DATABITS_8) {
            throw new IllegalArgumentException("Invalid data bits: " + dataBits);
        }
        lineRequestData[6] = (byte) dataBits;

        ctrlOut(SET_LINE_REQUEST, 0, 0, lineRequestData);

        resetDevice();

        mBaudRate = baudRate;
        mDataBits = dataBits;
        mStopBits = stopBits;
        mParity = parity;
    }

    private int filterBaudRate(int baudRate) {
//        if(BuildConfig.DEBUG && (baudRate & (3<<29)) == (1<<29)) {
//            return baudRate & ~(1<<29); // for testing purposes accept without further checks
//        }
        if (baudRate <= 0) {
            throw new IllegalArgumentException("Invalid baud rate: " + baudRate);
        }
        if (mDeviceType == DeviceType.DEVICE_TYPE_HXN) {
            return baudRate;
        }
        for(int br : standardBaudRates) {
            if (br == baudRate) {
                return baudRate;
            }
        }
        /*
         * Formula taken from Linux + FreeBSD.
         *
         * For TA+TB devices
         *   baudrate = baseline / (mantissa * 2^exponent)
         * where
         *   mantissa = buf[10:0]
         *   exponent = buf[15:13 16]
         *
         * For other devices
         *   baudrate = baseline / (mantissa * 4^exponent)
         * where
         *   mantissa = buf[8:0]
         *   exponent = buf[11:9]
         *
         */
        int baseline, mantissa, exponent, buf, effectiveBaudRate;
        baseline = 12000000 * 32;
        mantissa = baseline / baudRate;
        if (mantissa == 0) { // > unrealistic 384 MBaud
            throw new UnsupportedOperationException("Baud rate to high");
        }
        exponent = 0;
        if (mDeviceType == DeviceType.DEVICE_TYPE_T) {
            while (mantissa >= 2048) {
                if (exponent < 15) {
                    mantissa >>= 1;    /* divide by 2 */
                    exponent++;
                } else { // < 7 baud
                    throw new UnsupportedOperationException("Baud rate to low");
                }
            }
            buf = mantissa + ((exponent & ~1) << 12) + ((exponent & 1) << 16) + (1 << 31);
            effectiveBaudRate = (baseline / mantissa) >> exponent;
        } else {
            while (mantissa >= 512) {
                if (exponent < 7) {
                    mantissa >>= 2;    /* divide by 4 */
                    exponent++;
                } else { // < 45.8 baud
                    throw new UnsupportedOperationException("Baud rate to low");
                }
            }
            buf = mantissa + (exponent << 9) + (1 << 31);
            effectiveBaudRate = (baseline / mantissa) >> (exponent << 1);
        }
        double baudRateError = Math.abs(1.0 - (effectiveBaudRate / (double)baudRate));
        if(baudRateError >= 0.031) // > unrealistic 11.6 Mbaud
            throw new UnsupportedOperationException(String.format("Baud rate deviation %.1f%% is higher than allowed 3%%", baudRateError*100));

        Log.d(TAG, String.format("baud rate=%d, effective=%d, error=%.1f%%, value=0x%08x, mantissa=%d, exponent=%d",
                baudRate, effectiveBaudRate, baudRateError*100, buf, mantissa, exponent));
        return buf;
    }

    private boolean testStatusFlag(int flag) throws IOException {
        return ((getStatus() & flag) == flag);
    }

    private int getStatus() throws IOException {
        if ((mReadStatusThread == null) && (mReadStatusException == null)) {
            synchronized (mReadStatusThreadLock) {
                if (mReadStatusThread == null) {
                    mStatus = 0;
                    if(mDeviceType == DeviceType.DEVICE_TYPE_HXN) {
                        byte[] data = vendorIn(GET_CONTROL_HXN_REQUEST, 0, 1);
                        if ((data[0] & GET_CONTROL_HXN_FLAG_CTS) == 0) mStatus |= STATUS_FLAG_CTS;
                        if ((data[0] & GET_CONTROL_HXN_FLAG_DSR) == 0) mStatus |= STATUS_FLAG_DSR;
                        if ((data[0] & GET_CONTROL_HXN_FLAG_CD) == 0) mStatus |= STATUS_FLAG_CD;
                        if ((data[0] & GET_CONTROL_HXN_FLAG_RI) == 0) mStatus |= STATUS_FLAG_RI;
                    } else {
                        byte[] data = vendorIn(GET_CONTROL_REQUEST, 0, 1);
                        if ((data[0] & GET_CONTROL_FLAG_CTS) == 0) mStatus |= STATUS_FLAG_CTS;
                        if ((data[0] & GET_CONTROL_FLAG_DSR) == 0) mStatus |= STATUS_FLAG_DSR;
                        if ((data[0] & GET_CONTROL_FLAG_CD) == 0) mStatus |= STATUS_FLAG_CD;
                        if ((data[0] & GET_CONTROL_FLAG_RI) == 0) mStatus |= STATUS_FLAG_RI;
                    }
                    //Log.d(TAG, "start control line status thread " + mStatus);
                    mReadStatusThread = new Thread(this::readStatusThreadFunction);
                    mReadStatusThread.setDaemon(true);
                    mReadStatusThread.start();
                }
            }
        }

        /* throw and clear an exception which occured in the status read thread */
        IOException readStatusException = mReadStatusException;
        if (mReadStatusException != null) {
            mReadStatusException = null;
            throw new IOException(readStatusException);
        }

        return mStatus;
    }


    private void readStatusThreadFunction() {
        try {
            while (!mStopReadStatusThread) {
                byte[] buffer = new byte[STATUS_BUFFER_SIZE];
                long endTime = MonotonicClock.millis() + 500;
                int readBytesCount = mConnection.bulkTransfer(mInterruptEndpoint, buffer, STATUS_BUFFER_SIZE, 500);
                if(readBytesCount == -1 && MonotonicClock.millis() < endTime)
                    testConnection();
                if (readBytesCount > 0) {
                    if (readBytesCount != STATUS_BUFFER_SIZE) {
                        throw new IOException("Invalid status notification, expected " + STATUS_BUFFER_SIZE + " bytes, got " + readBytesCount);
                    } else if(buffer[0] != (byte)STATUS_NOTIFICATION ) {
                        throw new IOException("Invalid status notification, expected " + STATUS_NOTIFICATION + " request, got " + buffer[0]);
                    } else {
                        mStatus = buffer[STATUS_BYTE_IDX] & 0xff;
                    }
                }
            }
        } catch (IOException e) {
            mReadStatusException = e;
        }
        //Log.d(TAG, "end control line status thread " + mStopReadStatusThread + " " + (mReadStatusException == null ? "-" : mReadStatusException.getMessage()));
    }

    @Override
    public boolean getCD() throws IOException {
        return testStatusFlag(STATUS_FLAG_CD);    }

    @Override
    public boolean getCTS() throws IOException {
        return testStatusFlag(STATUS_FLAG_CTS);
    }

    @Override
    public boolean getDSR() throws IOException {
        return testStatusFlag(STATUS_FLAG_DSR);
    }

    @Override
    public boolean getDTR() throws IOException {
        return (mControlLinesValue & CONTROL_DTR) != 0;
    }

    @Override
    public void setDTR(boolean value) throws IOException {
        int newControlLinesValue;
        if (value) {
            newControlLinesValue = mControlLinesValue | CONTROL_DTR;
        } else {
            newControlLinesValue = mControlLinesValue & ~CONTROL_DTR;
        }
        setControlLines(newControlLinesValue);
    }

    @Override
    public boolean getRI() throws IOException {
        return testStatusFlag(STATUS_FLAG_RI);
    }

    @Override
    public boolean getRTS() throws IOException {
        return (mControlLinesValue & CONTROL_RTS) != 0;
    }

    @Override
    public void setRTS(boolean value) throws IOException {
        int newControlLinesValue;
        if (value) {
            newControlLinesValue = mControlLinesValue | CONTROL_RTS;
        } else {
            newControlLinesValue = mControlLinesValue & ~CONTROL_RTS;
        }
        setControlLines(newControlLinesValue);
    }

    public static Map<Integer, int[]> getSupportedDevices() {
        final Map<Integer, int[]> supportedDevices = new LinkedHashMap<>();
        supportedDevices.put(UsbId.VENDOR_PROLIFIC,
                new int[] {
                        UsbId.PROLIFIC_PL2303,
                        UsbId.PROLIFIC_PL2303GC,
                        UsbId.PROLIFIC_PL2303GB,
                        UsbId.PROLIFIC_PL2303GT,
                        UsbId.PROLIFIC_PL2303GL,
                        UsbId.PROLIFIC_PL2303GE,
                        UsbId.PROLIFIC_PL2303GS,
                });
        return supportedDevices;
    }
}
